import type { DirectiveBinding, ObjectDirective } from 'vue'
import { on } from './../js/utils/dom'
import { inBrowser } from './../js/utils/is'

type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void

type FlushList = Map<HTMLElement, {
  documentHandler: DocumentHandler
  bindingFn: (...args: unknown[]) => unknown
}>

const nodeList: FlushList = new Map()

let startClick: MouseEvent

if(inBrowser) {
  on(document, 'mousedown', (e: Event) => (startClick = e as MouseEvent))
  on(document, 'mouseup', (e: Event) => {
    for(const { documentHandler } of nodeList.values()) {
      documentHandler(e as MouseEvent, startClick)
    }
  })
}

function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding ): DocumentHandler {
  let excludes: HTMLElement[] = [] // 排除的元素
  if(Array.isArray(binding.arg)) {
    excludes = binding.arg
  } else {
    excludes.push(binding.arg as unknown as HTMLElement)
  }

  return function(mouseup, mousedown) {
    const mouseUpTarget = mouseup.target as Node
    const mouseDownTarget = mousedown.target as Node
    const isBound = !binding || !binding.instance
    const isTargetExists = !mouseUpTarget || !mouseDownTarget
    const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget)
    const isSelf = el === mouseUpTarget

    const isTargetExcluded =
      ( excludes.length &&
        excludes.some(item => item?.contains(mouseUpTarget))
      ) || (
        excludes.length && excludes.includes(mouseDownTarget as HTMLElement)
      )
    if (
      isBound ||
      isTargetExists ||
      isContainedByEl ||
      isSelf ||
      isTargetExcluded
    ) {
      return
    }
    binding.value()
  }
}

const ClickOutside: ObjectDirective = {
  beforeMount(el, binding) {
    nodeList.set(el, {
      documentHandler: createDocumentHandler(el, binding),
      bindingFn: binding.value
    })
  },
  updated(el, binding) {
    nodeList.set(el, {
      documentHandler: createDocumentHandler(el, binding),
      bindingFn: binding.value
    })
  },
  unmounted(el) {
    nodeList.delete(el)
  }
}

export default ClickOutside
